--- title: "02-SpringBoot MVC 分层最佳实践" created: 2025-12-02 tags: - 项目 aliases: - SpringBoot MVC 分层最佳实践 --- # SpringBoot MVC 分层最佳实践 ## **1. 项目结构规范** ### **1.1 推荐目录结构** ```text src/main/java/com/example/project/ ├── controller/ # 控制器层 │ └── UserController.java ├── service/ # 服务层接口 │ └── UserService.java ├── service/impl/ # 服务层实现 │ └── UserServiceImpl.java ├── mapper/ # MyBatis Mapper 接口 │ └── UserMapper.java ├── model/ # 模型层(model/pojo) │ ├── entity/ # 数据库实体(entity/domain) │ │ └── User.java │ ├── dto/ # 数据传输对象(请求) │ │ └── user/ │ │ ├── UserRegisterDTO.java │ │ ├── UserLoginDTO.java │ │ └── UserUpdateDTO.java │ ├── vo/ # 视图对象(响应) │ │ └── UserVO.java │ ├── query/ # 查询对象 │ │ └── UserQueryDTO.java │ └── enums/ # 枚举类 │ ├── GenderEnum.java │ ├── UserStatusEnum.java │ └── UserRoleEnum.java ├── common/ # 公共模块 │ ├── BaseResponse.java │ ├── ErrorCode.java │ ├── ResultUtils.java │ └── PageResult.java ├── exception/ # 异常处理 │ ├── BusinessException.java │ └── GlobalExceptionHandler.java ├── config/ # 配置类 │ └── MyBatisPlusConfig.java ├── utils/ # 工具类 │ └── PasswordUtils.java └── validation/ # 自定义校验器 └── groups/ └── UpdateGroup.java ``` ### **1.2 命名规范演进** | **旧名称** | **新名称(推荐)** | **说明** | | --- | --- | --- | | POJO | Model | 更通用的术语 | | Domain | Entity | 明确表示数据库实体 | | Request | DTO | 数据传输对象,用于请求 | | Response | VO | 视图对象,用于响应 | --- ## **2. 实体类设计规范** ### **2.1 数据库设计原则** ```sql -- 推荐:字段使用下划线命名 CREATE TABLE `user` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', `user_name` VARCHAR(50) NOT NULL COMMENT '用户昵称', `user_account` VARCHAR(50) NOT NULL COMMENT '账号', `user_password` VARCHAR(256) NOT NULL COMMENT '密码', `avatar_url` VARCHAR(512) DEFAULT NULL COMMENT '头像URL', `gender` VARCHAR(10) DEFAULT 'UNKNOWN' COMMENT '性别', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', `user_status` VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '用户状态', `user_role` VARCHAR(20) DEFAULT 'USER' COMMENT '用户角色', `tags` VARCHAR(1024) DEFAULT NULL COMMENT '标签JSON', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_delete` TINYINT DEFAULT 0 COMMENT '逻辑删除标识', UNIQUE KEY `uk_user_account` (`user_account`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; ``` ### **2.2 Entity 实体类** ```java package com.zwnsyw.zwwwspringbootbasetemplate.model.entity; import com.baomidou.mybatisplus.annotation.*; import com.zwnsyw.zwwwspringbootbasetemplate.model.enums.GenderEnum; import com.zwnsyw.zwwwspringbootbasetemplate.model.enums.UserRoleEnum; import com.zwnsyw.zwwwspringbootbasetemplate.model.enums.UserStatusEnum; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; /** * 用户实体 * * @author Zwww */ @Data @TableName("user") public class User implements Serializable { private static final long serialVersionUID = 1L; /** * 主键ID */ @TableId(type = IdType.ASSIGN_ID) private Long id; /** * 用户昵称 */ private String userName; /** * 账号 */ private String userAccount; /** * 密码(加密存储) */ private String userPassword; /** * 头像URL */ private String avatarUrl; /** * 性别 */ private GenderEnum gender; /** * 手机号 */ private String phone; /** * 邮箱 */ private String email; /** * 用户状态 */ private UserStatusEnum userStatus; /** * 用户角色 */ private UserRoleEnum userRole; /** * 标签JSON */ private String tags; /** * 创建时间 */ private LocalDateTime createTime; /** * 更新时间 */ private LocalDateTime updateTime; /** * 逻辑删除标识(0-未删除,1-已删除) */ @TableLogic private Integer isDelete; } ``` ### **2.3 Entity 设计原则** | **原则** | **说明** | | --- | --- | | 不放校验注解 | Entity 是数据库映射,校验逻辑放在 DTO | | 使用枚举类型 | 固定选项字段使用枚举,提高类型安全 | | 使用 LocalDateTime | 替代 Date,更现代的时间 API | | 逻辑删除字段 | 使用 @TableLogic 注解 | | 序列化支持 | 实现 Serializable 接口 | --- ## **3. 枚举设计规范** ### **3.1 用户性别枚举** ```java package com.zwnsyw.zwwwspringbootbasetemplate.model.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; /** * 性别枚举 */ public enum GenderEnum { MALE("MALE", "男"), FEMALE("FEMALE", "女"), UNKNOWN("UNKNOWN", "保密"); @EnumValue private final String code; private final String description; GenderEnum(String code, String description) { this.code = code; this.description = description; } @JsonValue public String getCode() { return code; } public String getDescription() { return description; } @JsonCreator public static GenderEnum fromCode(String code) { if (code == null) { return UNKNOWN; } for (GenderEnum gender : values()) { if (gender.code.equalsIgnoreCase(code)) { return gender; } } return UNKNOWN; } public static boolean isValid(String code) { for (GenderEnum gender : values()) { if (gender.code.equalsIgnoreCase(code)) { return true; } } return false; } } ``` ### **3.2 用户状态枚举** ```java package com.zwnsyw.zwwwspringbootbasetemplate.model.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; /** * 用户状态枚举 */ public enum UserStatusEnum { ACTIVE("ACTIVE", "正常"), DISABLED("DISABLED", "禁用"), LOCKED("LOCKED", "锁定"); @EnumValue private final String code; private final String description; UserStatusEnum(String code, String description) { this.code = code; this.description = description; } @JsonValue public String getCode() { return code; } public String getDescription() { return description; } @JsonCreator public static UserStatusEnum fromCode(String code) { if (code == null) { return ACTIVE; } for (UserStatusEnum status : values()) { if (status.code.equalsIgnoreCase(code)) { return status; } } throw new IllegalArgumentException("未知的用户状态: " + code); } } ``` ### **3.3 用户角色枚举** ```java package com.zwnsyw.zwwwspringbootbasetemplate.model.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; @Getter public enum UserRoleEnum { USER("USER", "普通用户"), ADMIN("ADMIN", "管理员"), BAN("BAN", "封禁用户"); @EnumValue private final String code; private final String description; UserRoleEnum(String code, String description) { this.code = code; this.description = description; } @JsonValue public String getCode() { return code; } @JsonCreator public static UserRoleEnum fromCode(String code) { if (code == null) { return null; } for (UserRoleEnum role : values()) { if (role.code.equalsIgnoreCase(code)) { return role; } } throw new IllegalArgumentException("未知的用户角色: " + code); } public boolean isAdmin() { return this == ADMIN; } public boolean isBanned() { return this == BAN; } public boolean isUser() { return this == USER; } } ``` ### **3.4 枚举设计总结** | **注解** | **作用** | **使用位置** | | --- | --- | --- | | @EnumValue | 指定存储到数据库的字段 | code 字段 | | @JsonValue | 指定序列化到 JSON 的字段 | description 字段 | | @JsonCreator | 指定反序列化时的工厂方法 | fromCode 方法 | --- ## **4. DTO/VO 设计规范** ### **4.1 DTO(Data Transfer Object)- 请求数据** #### **用户注册 DTO** ```java package com.example.project.model.dto.user; import lombok.Data; import javax.validation.constraints.*; import java.io.Serializable; /** * 用户注册请求 */ @Data public class UserRegisterDTO implements Serializable { private static final long serialVersionUID = 1L; /** * 账号 */ @NotBlank(message = "账号不能为空") @Size(min = 4, max = 20, message = "账号长度必须在4-20之间") @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "账号只能包含字母、数字和下划线") private String userAccount; /** * 密码 */ @NotBlank(message = "密码不能为空") @Size(min = 8, max = 20, message = "密码长度必须在8-20之间") @Pattern( regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]+$", message = "密码必须包含大小写字母和数字" ) private String userPassword; /** * 确认密码 */ @NotBlank(message = "确认密码不能为空") private String checkPassword; /** * 校验密码是否一致 */ @AssertTrue(message = "两次输入的密码不一致") public boolean isPasswordMatch() { if (userPassword == null || checkPassword == null) { return true; // 交给 @NotBlank 处理 } return userPassword.equals(checkPassword); } } ``` #### **用户登录 DTO** ```java package com.example.project.model.dto.user; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; import java.io.Serializable; /** * 用户登录请求 */ @Data public class UserLoginDTO implements Serializable { private static final long serialVersionUID = 1L; /** * 账号 */ @NotBlank(message = "账号不能为空") @Size(max = 20, message = "账号长度不能超过20") private String userAccount; /** * 密码 */ @NotBlank(message = "密码不能为空") @Size(min = 8, max = 20, message = "密码长度必须在8-20之间") private String userPassword; } ``` #### **用户更新 DTO(分组校验)** ```java package com.example.project.model.dto.user; import com.example.project.validation.groups.UpdateGroup; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; import javax.validation.constraints.*; import java.io.Serializable; /** * 用户信息更新请求 */ @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class UserUpdateDTO implements Serializable { private static final long serialVersionUID = 1L; /** * 用户ID(必填) */ @NotNull(message = "用户ID不能为空", groups = UpdateGroup.class) private Long id; /** * 用户昵称 */ @Size(max = 50, message = "用户名长度不能超过50", groups = UpdateGroup.class) private String userName; /** * 头像URL */ @Size(max = 512, message = "头像URL长度不能超过512", groups = UpdateGroup.class) private String avatarUrl; /** * 性别 */ private String gender; /** * 手机号 */ @Pattern( regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确", groups = UpdateGroup.class ) private String phone; /** * 邮箱 */ @Email(message = "邮箱格式不正确", groups = UpdateGroup.class) private String email; /** * 标签列表(JSON格式) */ @Size(max = 1024, message = "标签内容过长", groups = UpdateGroup.class) private String tags; } ``` #### **分组校验接口** ```java package com.example.project.validation.groups; /** * 更新操作的校验分组 */ public interface UpdateGroup { } /** * 创建操作的校验分组 */ public interface CreateGroup { } ``` ### **4.2 VO(View Object)- 响应数据** ```java package com.zwnsyw.zwwwspringbootbasetemplate.model.vo; import com.zwnsyw.zwwwspringbootbasetemplate.model.entity.User; import com.zwnsyw.zwwwspringbootbasetemplate.model.enums.GenderEnum; import com.zwnsyw.zwwwspringbootbasetemplate.model.enums.UserRoleEnum; import com.zwnsyw.zwwwspringbootbasetemplate.model.enums.UserStatusEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; /** * 用户视图对象(脱敏后返回给前端) */ @Data @Schema(description = "用户视图对象") public class UserVO implements Serializable { private static final long serialVersionUID = 1L; @Schema(description = "用户ID") private Long id; @Schema(description = "用户昵称") private String userName; @Schema(description = "用户账号") private String userAccount; @Schema(description = "头像URL") private String avatarUrl; @Schema(description = "性别") private GenderEnum gender; @Schema(description = "手机号(脱敏)", example = "138****8888") private String phone; @Schema(description = "邮箱(脱敏)", example = "u***@example.com") private String email; @Schema(description = "用户状态") private UserStatusEnum userStatus; @Schema(description = "用户角色") private UserRoleEnum userRole; @Schema(description = "标签JSON") private String tags; @Schema(description = "创建时间") private LocalDateTime createTime; /** * Entity 转 VO(带脱敏处理) */ public static UserVO fromEntity(User user) { if (user == null) { return null; } UserVO vo = new UserVO(); vo.setId(user.getId()); vo.setUserName(user.getUserName()); vo.setUserAccount(user.getUserAccount()); vo.setAvatarUrl(user.getAvatarUrl()); vo.setGender(user.getGender()); vo.setPhone(maskPhone(user.getPhone())); vo.setEmail(maskEmail(user.getEmail())); vo.setUserStatus(user.getUserStatus()); vo.setUserRole(user.getUserRole()); vo.setTags(user.getTags()); vo.setCreateTime(user.getCreateTime()); return vo; } /** * 手机号脱敏:138****8888 */ private static String maskPhone(String phone) { if (phone == null || phone.length() < 11) { return phone; } return phone.substring(0, 3) + "****" + phone.substring(7); } /** * 邮箱脱敏:u***@example.com */ private static String maskEmail(String email) { if (email == null || !email.contains("@")) { return email; } int atIndex = email.indexOf("@"); if (atIndex <= 1) { return email; } return email.charAt(0) + "***" + email.substring(atIndex); } } ``` ### **4.3 DTO/VO 设计对比** | **特性** | **DTO** | **VO** | | --- | --- | --- | | 用途 | 接收前端请求数据 | 返回给前端的响应数据 | | 校验注解 | ✅ 必须有 | ❌ 不需要 | | 脱敏处理 | ❌ 不需要 | ✅ 必须有 | | 字段范围 | 仅包含请求需要的字段 | 仅包含需要展示的字段 | | 敏感字段 | 可以包含(如密码) | 不能包含(如密码) | --- ## **5. 三层校验机制** ### **5.1 校验分层架构图** ```text ┌─────────────────────────────────────────────────────────────┐ │ 前端校验 │ │ (表单验证、即时反馈) │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ Controller 层校验 │ │ ├── 请求参数非空判断 │ │ ├── 触发 DTO 校验 (@Valid) │ │ └── 简单的参数合法性检查 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ DTO 层校验 │ │ ├── 字段非空校验 (@NotBlank, @NotNull) │ │ ├── 格式校验 (@Email, @Pattern) │ │ ├── 范围校验 (@Size, @Min, @Max) │ │ └── 复杂校验 (@AssertTrue 自定义方法) │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ Service 层校验 │ │ ├── 业务逻辑校验 │ │ ├── 数据库查询校验 (账号是否存在等) │ │ ├── 权限校验 │ │ └── 复杂业务规则校验 │ └─────────────────────────────────────────────────────────────┘ ``` ### **5.2 Controller 层** ```java package com.zwnsyw.zwwwspringbootbasetemplate.controller; import com.zwnsyw.zwwwspringbootbasetemplate.common.response.BaseResponse; import com.zwnsyw.zwwwspringbootbasetemplate.common.response.ResultUtils; import com.zwnsyw.zwwwspringbootbasetemplate.model.dto.UserLoginDTO; import com.zwnsyw.zwwwspringbootbasetemplate.model.dto.UserRegisterDTO; import com.zwnsyw.zwwwspringbootbasetemplate.model.dto.UserUpdateDTO; import com.zwnsyw.zwwwspringbootbasetemplate.model.vo.LoginUserVO; import com.zwnsyw.zwwwspringbootbasetemplate.model.vo.UserVO; import com.zwnsyw.zwwwspringbootbasetemplate.service.UserService; import com.zwnsyw.zwwwspringbootbasetemplate.validation.groups.UpdateGroup; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; /** * 用户控制器 * * @author Zwww */ @RestController @RequestMapping("/user") @RequiredArgsConstructor @Tag(name = "用户模块", description = "用户注册、登录、信息管理等接口") public class UserController { private final UserService userService; /** * 用户注册 */ @PostMapping("/register") @Operation(summary = "用户注册", description = "通过账号密码注册新用户") public BaseResponse register(@RequestBody @Valid UserRegisterDTO dto) { // @Valid 已完成参数校验,dto 不会为 null(Spring 自动处理) Long userId = userService.register(dto); return ResultUtils.success(userId); } /** * 用户登录 */ @PostMapping("/login") @Operation(summary = "用户登录", description = "通过账号密码登录,成功后会创建 Session") public BaseResponse login( @RequestBody @Valid UserLoginDTO dto, HttpServletRequest request) { UserVO userVO = userService.login(dto, request); return ResultUtils.success(userVO); } /** * 更新用户信息 */ @PostMapping("/update") @Operation(summary = "更新用户信息", description = "更新当前登录用户或指定用户的信息(管理员)") public BaseResponse update( @RequestBody @Validated(UpdateGroup.class) UserUpdateDTO dto, HttpServletRequest request) { LoginUserVO loginUser = userService.getLoginUser(request); Boolean result = userService.updateUser(dto, loginUser); return ResultUtils.success(result); } /** * 获取当前登录用户 */ @GetMapping("/current") @Operation(summary = "获取当前登录用户", description = "获取当前登录用户的详细信息(从数据库实时查询)") public BaseResponse getCurrentUser(HttpServletRequest request) { UserVO userVO = userService.getCurrentUserVO(request); return ResultUtils.success(userVO); } /** * 用户退出登录 */ @PostMapping("/logout") @Operation(summary = "用户退出登录", description = "清除当前用户的登录状态") public BaseResponse logout(HttpServletRequest request) { Boolean result = userService.logout(request); return ResultUtils.success(result); } /** * 根据ID获取用户信息(公开接口) */ @GetMapping("/{id}") @Operation(summary = "根据ID获取用户", description = "获取指定用户的公开信息") public BaseResponse getUserById( @Parameter(description = "用户ID") @PathVariable Long id) { UserVO userVO = userService.getUserVOById(id); return ResultUtils.success(userVO); } } ``` ### **5.3 Service 层** ```java package com.zwnsyw.zwwwspringbootbasetemplate.service; import com.baomidou.mybatisplus.extension.service.IService; import com.zwnsyw.zwwwspringbootbasetemplate.model.dto.UserLoginDTO; import com.zwnsyw.zwwwspringbootbasetemplate.model.dto.UserRegisterDTO; import com.zwnsyw.zwwwspringbootbasetemplate.model.dto.UserUpdateDTO; import com.zwnsyw.zwwwspringbootbasetemplate.model.entity.User; import com.zwnsyw.zwwwspringbootbasetemplate.model.vo.LoginUserVO; import com.zwnsyw.zwwwspringbootbasetemplate.model.vo.UserVO; import javax.servlet.http.HttpServletRequest; /** * 用户服务接口 * * @author Zwww */ public interface UserService extends IService { /** * 用户注册 * * @param dto 注册信息 * @return 新用户ID */ Long register(UserRegisterDTO dto); /** * 用户登录 * * @param dto 登录信息 * @param request HTTP请求 * @return 用户视图对象 */ UserVO login(UserLoginDTO dto, HttpServletRequest request); /** * 更新用户信息 * * @param dto 更新信息 * @param loginUser 当前登录用户 * @return 是否成功 */ Boolean updateUser(UserUpdateDTO dto, LoginUserVO loginUser); /** * 获取当前登录用户(Session 中的简化信息) * * @param request HTTP请求 * @return 登录用户信息 */ LoginUserVO getLoginUser(HttpServletRequest request); /** * 获取当前登录用户的完整 VO(从数据库查询) * * @param request HTTP请求 * @return 用户视图对象 */ UserVO getCurrentUserVO(HttpServletRequest request); /** * 根据ID获取用户 VO * * @param id 用户ID * @return 用户视图对象 */ UserVO getUserVOById(Long id); /** * 用户退出登录 * * @param request HTTP请求 * @return 是否成功 */ Boolean logout(HttpServletRequest request); } ``` ```java package com.zwnsyw.zwwwspringbootbasetemplate.service.serviceimpl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.zwnsyw.zwwwspringbootbasetemplate.exception.ErrorCode; import com.zwnsyw.zwwwspringbootbasetemplate.exception.ThrowUtils; import com.zwnsyw.zwwwspringbootbasetemplate.mapper.UserMapper; import com.zwnsyw.zwwwspringbootbasetemplate.model.dto.UserLoginDTO; import com.zwnsyw.zwwwspringbootbasetemplate.model.dto.UserRegisterDTO; import com.zwnsyw.zwwwspringbootbasetemplate.model.dto.UserUpdateDTO; import com.zwnsyw.zwwwspringbootbasetemplate.model.entity.User; import com.zwnsyw.zwwwspringbootbasetemplate.model.enums.GenderEnum; import com.zwnsyw.zwwwspringbootbasetemplate.model.enums.UserRoleEnum; import com.zwnsyw.zwwwspringbootbasetemplate.model.enums.UserStatusEnum; import com.zwnsyw.zwwwspringbootbasetemplate.model.vo.LoginUserVO; import com.zwnsyw.zwwwspringbootbasetemplate.model.vo.UserVO; import com.zwnsyw.zwwwspringbootbasetemplate.service.UserService; import com.zwnsyw.zwwwspringbootbasetemplate.utils.PasswordUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; /** * 用户服务实现 * * @author Zwww */ @Slf4j @Service @RequiredArgsConstructor public class UserServiceImpl extends ServiceImpl implements UserService { private static final String USER_LOGIN_STATE = "USER_LOGIN_STATE"; /** * 账号正则:只允许字母、数字、下划线 */ private static final String ACCOUNT_PATTERN = "^[a-zA-Z0-9_]+$"; private final PasswordUtils passwordUtils; @Override public Long register(UserRegisterDTO dto) { String userAccount = dto.getUserAccount(); String userPassword = dto.getUserPassword(); // ============ Service 层:业务级校验 ============ // 1. 账号格式校验 ThrowUtils.throwIf( !userAccount.matches(ACCOUNT_PATTERN), ErrorCode.USER_ACCOUNT_INVALID, "账号只能包含字母、数字、下划线" ); // 2. 账号是否已存在 ThrowUtils.throwIf( isAccountExists(userAccount), ErrorCode.USER_ACCOUNT_ALREADY_EXISTS, String.format("账号 '%s' 已被注册", userAccount) ); // 3. 加密密码 String encryptedPassword = passwordUtils.encrypt(userPassword); // 4. 创建用户 User user = new User(); user.setUserAccount(userAccount); user.setUserPassword(encryptedPassword); user.setUserName("用户" + System.currentTimeMillis()); user.setGender(GenderEnum.UNKNOWN); user.setUserStatus(UserStatusEnum.ACTIVE); user.setUserRole(UserRoleEnum.USER); boolean saved = this.save(user); ThrowUtils.throwIf(!saved, ErrorCode.USER_REGISTER_FAILED, "注册失败,请稍后重试"); log.info("用户注册成功,userId: {}, account: {}", user.getId(), userAccount); return user.getId(); } @Override public UserVO login(UserLoginDTO dto, HttpServletRequest request) { String userAccount = dto.getUserAccount(); String userPassword = dto.getUserPassword(); // ============ Service 层:业务级校验 ============ // 1. 查询用户 User user = getByAccount(userAccount); if (user == null) { log.info("用户登录失败,账号不存在: {}", userAccount); // 统一返回"账号或密码错误",避免暴露账号是否存在 throw new com.zwnsyw.zwwwspringbootbasetemplate.exception.BusinessException( ErrorCode.USER_LOGIN_FAILED, "账号或密码错误" ); } // 2. 校验密码 if (!passwordUtils.verify(userPassword, user.getUserPassword())) { log.info("用户登录失败,密码错误: {}", userAccount); throw new com.zwnsyw.zwwwspringbootbasetemplate.exception.BusinessException( ErrorCode.USER_LOGIN_FAILED, "账号或密码错误" ); } // 3. 校验用户状态 ThrowUtils.throwIf( user.getUserStatus() == UserStatusEnum.DISABLED, ErrorCode.USER_ACCOUNT_DISABLED, String.format("账号 '%s' 已被禁用,如有疑问请联系管理员", userAccount) ); ThrowUtils.throwIf( user.getUserStatus() == UserStatusEnum.LOCKED, ErrorCode.USER_ACCOUNT_LOCKED, String.format("账号 '%s' 已被锁定,请稍后重试", userAccount) ); // 4. 存储登录态(只存储必要信息) HttpSession session = request.getSession(); LoginUserVO loginUserVO = LoginUserVO.fromEntity(user); session.setAttribute(USER_LOGIN_STATE, loginUserVO); log.info("用户登录成功,userId: {}, account: {}", user.getId(), userAccount); return UserVO.fromEntity(user); } @Override public Boolean updateUser(UserUpdateDTO dto, LoginUserVO loginUser) { Long targetUserId = dto.getId(); // ============ Service 层:业务级校验 ============ // 1. 权限校验:只能修改自己的信息,管理员可以修改任何人 boolean isSelf = targetUserId.equals(loginUser.getId()); boolean isAdmin = loginUser.getUserRole() != null && loginUser.getUserRole().isAdmin(); ThrowUtils.throwIf( !isSelf && !isAdmin, ErrorCode.USER_NO_PERMISSION, "无权限修改其他用户信息" ); // 2. 校验目标用户是否存在 User targetUser = ThrowUtils.throwIfNull( this.getById(targetUserId), ErrorCode.USER_NOT_FOUND, String.format("用户 ID=%d 不存在", targetUserId) ); // 3. 更新字段(仅更新非空字段) updateUserFields(dto, targetUser); boolean updated = this.updateById(targetUser); ThrowUtils.throwIf(!updated, ErrorCode.USER_UPDATE_FAILED, "更新用户信息失败"); log.info("用户信息更新成功,targetUserId: {}, operatorId: {}", targetUserId, loginUser.getId()); return true; } @Override public LoginUserVO getLoginUser(HttpServletRequest request) { HttpSession session = request.getSession(false); ThrowUtils.throwIf(session == null, ErrorCode.USER_NOT_LOGIN, "请先登录"); Object userObj = session.getAttribute(USER_LOGIN_STATE); ThrowUtils.throwIfNull(userObj, ErrorCode.USER_NOT_LOGIN, "请先登录"); return (LoginUserVO) userObj; } @Override public UserVO getCurrentUserVO(HttpServletRequest request) { LoginUserVO loginUser = getLoginUser(request); // 从数据库获取最新信息 User user = ThrowUtils.throwIfNull( this.getById(loginUser.getId()), ErrorCode.USER_NOT_FOUND, "用户不存在或已被删除" ); return UserVO.fromEntity(user); } @Override public UserVO getUserVOById(Long id) { ThrowUtils.throwIfNull(id, ErrorCode.PARAMS_ERROR, "用户ID不能为空"); User user = ThrowUtils.throwIfNull( this.getById(id), ErrorCode.USER_NOT_FOUND, String.format("用户 ID=%d 不存在", id) ); return UserVO.fromEntity(user); } @Override public Boolean logout(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session != null) { session.removeAttribute(USER_LOGIN_STATE); log.info("用户退出登录成功"); } return true; } // ============ 私有辅助方法 ============ /** * 根据账号查询用户 */ private User getByAccount(String userAccount) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserAccount, userAccount); return this.getOne(queryWrapper); } /** * 检查账号是否已存在 */ private boolean isAccountExists(String userAccount) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserAccount, userAccount); return this.count(queryWrapper) > 0; } /** * 检查手机号是否被其他用户使用 */ private boolean isPhoneUsedByOther(String phone, Long excludeUserId) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getPhone, phone) .ne(User::getId, excludeUserId); return this.count(queryWrapper) > 0; } /** * 检查邮箱是否被其他用户使用 */ private boolean isEmailUsedByOther(String email, Long excludeUserId) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getEmail, email) .ne(User::getId, excludeUserId); return this.count(queryWrapper) > 0; } /** * 更新用户字段 */ private void updateUserFields(UserUpdateDTO dto, User targetUser) { Long userId = targetUser.getId(); if (StringUtils.isNotBlank(dto.getUserName())) { targetUser.setUserName(dto.getUserName()); } if (StringUtils.isNotBlank(dto.getAvatarUrl())) { targetUser.setAvatarUrl(dto.getAvatarUrl()); } if (StringUtils.isNotBlank(dto.getGender())) { targetUser.setGender(GenderEnum.fromCode(dto.getGender())); } if (StringUtils.isNotBlank(dto.getPhone())) { ThrowUtils.throwIf( isPhoneUsedByOther(dto.getPhone(), userId), ErrorCode.USER_PHONE_ALREADY_EXISTS, String.format("手机号 '%s' 已被其他用户使用", dto.getPhone()) ); targetUser.setPhone(dto.getPhone()); } if (StringUtils.isNotBlank(dto.getEmail())) { ThrowUtils.throwIf( isEmailUsedByOther(dto.getEmail(), userId), ErrorCode.USER_EMAIL_ALREADY_EXISTS, String.format("邮箱 '%s' 已被其他用户使用", dto.getEmail()) ); targetUser.setEmail(dto.getEmail()); } if (StringUtils.isNotBlank(dto.getTags())) { targetUser.setTags(dto.getTags()); } } } ``` #### `PasswordUtils` ```java package com.zwnsyw.zwwwspringbootbasetemplate.utils; import com.zwnsyw.zwwwspringbootbasetemplate.config.AppProperties; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.mindrot.jbcrypt.BCrypt; import org.springframework.stereotype.Component; /** * 密码加密工具类 *

* 使用 BCrypt 算法 + 静态盐进行密码加密 * BCrypt 本身会生成随机盐并存储在哈希结果中,静态盐提供额外的安全层 *

* * @author zwnsyw */ @Slf4j @Component @RequiredArgsConstructor public class PasswordUtils { private final AppProperties appProperties; /** * 加密密码 * * @param rawPassword 明文密码 * @return 加密后的密码 */ public String encrypt(String rawPassword) { if (rawPassword == null || rawPassword.isEmpty()) { throw new IllegalArgumentException("密码不能为空"); } String enhancedPassword = enhance(rawPassword); String salt = BCrypt.gensalt(appProperties.getSecurity().getBcryptStrength()); return BCrypt.hashpw(enhancedPassword, salt); } /** * 验证密码 * * @param rawPassword 用户输入的明文密码 * @param encryptedPassword 数据库中存储的加密密码 * @return 是否匹配 */ public boolean verify(String rawPassword, String encryptedPassword) { if (rawPassword == null || encryptedPassword == null) { return false; } try { String enhancedPassword = enhance(rawPassword); return BCrypt.checkpw(enhancedPassword, encryptedPassword); } catch (Exception e) { log.warn("密码验证异常: {}", e.getMessage()); return false; } } /** * 增强密码(添加静态盐) */ private String enhance(String rawPassword) { return appProperties.getSecurity().getPasswordSalt() + rawPassword; } /** * 检查密码是否需要重新加密(用于密码策略升级) * * @param encryptedPassword 加密后的密码 * @return 是否需要重新加密 */ public boolean needsRehash(String encryptedPassword) { if (encryptedPassword == null || !encryptedPassword.startsWith("$2")) { return true; } try { // BCrypt 哈希格式: $2a$10$... 其中 10 是强度 String[] parts = encryptedPassword.split("\\$"); if (parts.length >= 3) { int currentStrength = Integer.parseInt(parts[2]); return currentStrength < appProperties.getSecurity().getBcryptStrength(); } } catch (Exception e) { log.warn("解析密码哈希强度失败: {}", e.getMessage()); } return true; } } ``` ### **5.4 三层校验职责总结** | **层级** | **校验类型** | **示例** | | --- | --- | --- | | Controller | 参数非空、触发DTO校验 | `dto == null`、`@Valid` | | DTO | 字段格式、长度、范围 | `@NotBlank`、`@Size`、`@Pattern`、`@AssertTrue` | | Service | 业务逻辑、数据库校验 | 账号是否存在、密码是否正确、权限校验 | --- ## **6. 代码生成与配置** ### **6.1 Maven 依赖** ```xml 4.0.0 com.zwnsyw ZwwwSpringBootBaseTemplate 0.0.1-SNAPSHOT ZwwwSpringBootBaseTemplate ZwwwSpringBootBaseTemplate 1.8 UTF-8 UTF-8 2.7.6 3.5.3.1 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-validation com.baomidou mybatis-plus-boot-starter ${mybatis-plus.version} com.mysql mysql-connector-j runtime org.springframework.boot spring-boot-starter-data-redis org.springframework.session spring-session-data-redis org.springframework.boot spring-boot-starter-cache io.jsonwebtoken jjwt-api 0.11.5 io.jsonwebtoken jjwt-impl 0.11.5 runtime io.jsonwebtoken jjwt-jackson 0.11.5 runtime org.mindrot jbcrypt 0.4 org.projectlombok lombok true cn.hutool hutool-all 5.8.26 org.apache.commons commons-lang3 3.12.0 com.github.xiaoymin knife4j-openapi3-spring-boot-starter 4.4.0 com.squareup.okhttp3 okhttp 4.10.0 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.apache.maven.plugins maven-compiler-plugin 3.8.1 1.8 1.8 UTF-8 org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} com.zwnsyw.zwwwspringbootbasetemplate.ZwwwSpringBootBaseTemplateApplication true repackage repackage ``` ### **6.2 Spring Boot YAML 配置详解与多环境管理** #### `application.yml`**(主配置文件)** ```yaml # ==================== 服务器配置 ==================== server: port: 8080 servlet: context-path: /api tomcat: max-http-form-post-size: 100MB max-swallow-size: -1 # ==================== Spring 核心配置 ==================== spring: application: name: project-name # 多环境配置 - 默认激活 dev profiles: active: ${SPRING_PROFILES_ACTIVE:dev} # -------------------- 数据源配置 -------------------- datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/database_name?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_password # -------------------- 文件上传配置 -------------------- servlet: multipart: enabled: true max-file-size: 10MB max-request-size: 50MB # -------------------- Redis 配置 -------------------- redis: host: localhost port: 6379 database: 0 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1ms # -------------------- 缓存配置 -------------------- cache: type: redis redis: time-to-live: 300s cache-null-values: true # -------------------- Session 配置 -------------------- session: store-type: redis redis: namespace: ${spring.application.name}:session flush-mode: on_save timeout: 86400s # ==================== MyBatis Plus 配置 ==================== mybatis-plus: configuration: map-underscore-to-camel-case: true default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler global-config: db-config: logic-delete-field: isDeleted logic-delete-value: 1 logic-not-delete-value: 0 # mapper-locations: classpath*:/mapper/**/*.xml # ==================== 接口文档配置(Knife4j) ==================== knife4j: enable: true openapi: title: "${app.name} 接口文档" description: "API 接口说明文档" version: ${app.version} group: default: api-rule: package api-rule-resources: - com.example.projectname.controller # ==================== 自定义应用配置(AppProperties) ==================== app: name: project-name version: 1.0.0 debug: false # 文件上传配置 file: max-size: 10485760 # 10MB allowed-formats: jpg,jpeg,png,gif,webp,pdf upload-path: /uploads # JWT 配置 jwt: secret: ${JWT_SECRET:your-default-secret-key-please-change-in-production} expiration: 604800 # 7天(秒) token-prefix: "Bearer " header-name: Authorization # 用户相关配置 user: max-password-retry: 5 max-login-device: 3 lock-minutes: 30 # ==================== CORS 跨域配置 ==================== cors: # 允许的前端域名(多个用逗号分隔) allowed-origins: ${CORS_ORIGINS:http://localhost:5173,http://localhost:3000} # ==================== 日志配置 ==================== logging: level: root: INFO com.example.projectname: DEBUG pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" ``` #### `application-dev.yml`**(开发环境)** ```yaml # ==================== 开发环境配置 ==================== # 激活方式:spring.profiles.active=dev spring: datasource: url: jdbc:mysql://localhost:3306/dev_database?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: dev_password redis: host: localhost port: 6379 database: 0 # 开发环境开启 SQL 日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启接口文档 knife4j: enable: true # 应用配置 - 开发环境 app: debug: true jwt: # 开发环境使用较短的过期时间便于测试 expiration: 86400 # 1天 # CORS - 开发环境允许所有来源 cors: allowed-origins: "*" logging: level: com.example.projectname: DEBUG com.example.projectname.mapper: DEBUG # 开启 Mapper SQL 日志 ``` #### `application-test.yml`**(测试环境)** ```yaml # ==================== 测试环境配置 ==================== # 激活方式:spring.profiles.active=test spring: datasource: url: jdbc:mysql://${DB_HOST:test-db.example.com}:3306/test_database?useSSL=true&serverTimezone=Asia/Shanghai username: ${DB_USERNAME:test_user} password: ${DB_PASSWORD:test_password} redis: host: ${REDIS_HOST:test-redis.example.com} port: 6379 password: ${REDIS_PASSWORD:} database: 1 # 使用不同的数据库索引 # 测试环境关闭详细 SQL 日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl # 测试环境开启接口文档(便于测试人员使用) knife4j: enable: true app: debug: false jwt: secret: ${JWT_SECRET:test-environment-jwt-secret-key} cors: allowed-origins: https://test.example.com,https://test-admin.example.com logging: level: root: INFO com.example.projectname: DEBUG file: name: logs/application-test.log max-size: 50MB max-history: 15 ``` #### `application-prod.yml`**(生产环境)** ```yaml # ==================== 生产环境配置 ==================== # 激活方式:spring.profiles.active=prod server: tomcat: # 生产环境优化线程池 threads: max: 200 min-spare: 20 spring: datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT:3306}/${DB_NAME}?useSSL=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: ${DB_USERNAME} password: ${DB_PASSWORD} hikari: # 连接池优化 maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 300000 connection-timeout: 20000 max-lifetime: 1200000 redis: host: ${REDIS_HOST} port: ${REDIS_PORT:6379} password: ${REDIS_PASSWORD} database: 0 lettuce: pool: max-active: 16 max-idle: 8 min-idle: 4 # 生产环境关闭 SQL 日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl # 【重要】生产环境必须关闭接口文档 knife4j: enable: false app: debug: false jwt: # 【重要】生产环境必须通过环境变量配置 secret: ${JWT_SECRET} expiration: 604800 cors: # 生产环境严格限制允许的域名 allowed-origins: ${CORS_ORIGINS:https://www.example.com,https://admin.example.com} logging: level: root: WARN com.example.projectname: INFO file: name: /var/log/application/app.log max-size: 100MB max-history: 30 ``` [[04-多环境配置详解|多环境配置详解]] ### **6.3 配置类** #### `CacheConfig.java` ```java package com.zwnsyw.zwwwspringbootbasetemplate.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.cache.CacheManager; import java.time.Duration; import java.util.HashMap; import java.util.Map; /** * Spring Cache 缓存配置 *

* 配合 @Cacheable、@CacheEvict、@CachePut 等注解使用 *

* * @author your-name */ @Configuration public class CacheConfig { /** * 默认缓存过期时间(小时) */ private static final long DEFAULT_TTL_HOURS = 2; /** * 配置缓存管理器 *

* 支持: * - 自定义不同缓存名称的过期时间 * - JSON 序列化存储 * - 禁止缓存空值(防止缓存穿透可设为 true) *

* * @param connectionFactory Redis 连接工厂 * @return CacheManager 缓存管理器 */ @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { // 默认缓存配置 RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig() // 默认过期时间 .entryTtl(Duration.ofHours(DEFAULT_TTL_HOURS)) // 禁止缓存空值(设为 true 可防止缓存穿透) .disableCachingNullValues() // Key 使用 String 序列化 .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(RedisSerializer.string())) // Value 使用 JSON 序列化 .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); // 针对不同缓存名称配置不同的过期时间 Map cacheConfigurations = new HashMap<>(); // 示例:用户信息缓存 30 分钟 cacheConfigurations.put("user", defaultConfig.entryTtl(Duration.ofMinutes(30))); // 示例:热门数据缓存 10 分钟 cacheConfigurations.put("hot", defaultConfig.entryTtl(Duration.ofMinutes(10))); // 示例:字典数据缓存 24 小时 cacheConfigurations.put("dict", defaultConfig.entryTtl(Duration.ofHours(24))); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(defaultConfig) .withInitialCacheConfigurations(cacheConfigurations) .build(); } // ==================== 自定义 KeyGenerator 示例 ==================== /** * 自定义缓存 Key 生成器 *

* 使用方式:@Cacheable(cacheNames = "xxx", keyGenerator = "customKeyGenerator") *

* * @return KeyGenerator */ // @Bean("customKeyGenerator") // public KeyGenerator customKeyGenerator() { // return (target, method, params) -> { // StringBuilder sb = new StringBuilder(); // sb.append(target.getClass().getSimpleName()); // sb.append(":"); // sb.append(method.getName()); // for (Object param : params) { // sb.append(":").append(param.toString()); // } // return sb.toString(); // }; // } } ``` #### `CorsConfig.java` ```java package com.example.projectname.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 跨域资源共享(CORS)配置 *

* 解决前后端分离项目中的跨域访问问题 *

* * @author your-name */ @Configuration public class CorsConfig implements WebMvcConfigurer { /** * 允许的前端域名列表(从配置文件读取) *

* 配置示例:cors.allowed-origins=http://localhost:5173,https://example.com *

*/ @Value("${cors.allowed-origins:*}") private String[] allowedOrigins; /** * 预检请求缓存时间(秒) *

* 浏览器会缓存 OPTIONS 预检请求的结果,避免频繁发送预检请求 *

*/ private static final long MAX_AGE_SECONDS = 3600; @Override public void addCorsMappings(CorsRegistry registry) { registry // 对所有路径生效 .addMapping("/**") // 允许携带 Cookie 和认证信息 // 注意:设为 true 时,allowedOrigins 不能为 "*" .allowCredentials(true) // 允许的请求来源 // 方式1:使用 allowedOrigins 精确匹配 // .allowedOrigins("http://localhost:5173", "https://example.com") // 方式2:使用 allowedOriginPatterns 支持通配符 .allowedOriginPatterns(allowedOrigins) // 允许的 HTTP 方法 .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") // 允许的请求头 .allowedHeaders("*") // 或者精确指定: // .allowedHeaders("Content-Type", "Authorization", "X-Requested-With") // 允许前端访问的响应头 .exposedHeaders("X-Total-Count", "Content-Disposition") // 预检请求缓存时间 .maxAge(MAX_AGE_SECONDS); } } ``` #### `JsonConfig.java` ```java package com.example.projectname.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import org.springframework.boot.jackson.JsonComponent; import org.springframework.context.annotation.Bean; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * Jackson JSON 序列化配置 *

* 主要解决以下问题: * 1. Long 类型精度丢失(JS 最大安全整数为 2^53-1) * 2. LocalDateTime 等日期时间格式化 *

* * @author your-name */ @JsonComponent public class JsonConfig { /** * 日期格式 */ private static final String DATE_FORMAT = "yyyy-MM-dd"; /** * 日期时间格式 */ private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; /** * 配置全局 ObjectMapper * * @param builder Jackson2ObjectMapperBuilder * @return ObjectMapper */ @Bean public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper objectMapper = builder.createXmlMapper(false).build(); // ==================== Long 类型处理 ==================== // JavaScript 中 Number 类型最大安全整数为 2^53-1 (9007199254740991) // 雪花算法生成的 ID 为 19 位,超出范围会精度丢失 // 解决方案:将 Long 类型序列化为 String SimpleModule longModule = new SimpleModule(); longModule.addSerializer(Long.class, ToStringSerializer.instance); longModule.addSerializer(Long.TYPE, ToStringSerializer.instance); objectMapper.registerModule(longModule); // ==================== 日期时间处理 ==================== JavaTimeModule javaTimeModule = new JavaTimeModule(); // LocalDate 序列化/反序列化 javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT))); javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT))); // LocalDateTime 序列化/反序列化 javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))); objectMapper.registerModule(javaTimeModule); // 禁用日期时间戳格式(使用格式化字符串) objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return objectMapper; } } ``` #### `MyBatisPlusConfig.java` ```java package com.zwnsyw.zwwwspringbootbasetemplate.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.time.LocalDateTime; /** * MyBatis Plus 配置 *

* 包含: * - 分页插件 * - 乐观锁插件 * - 自动填充处理器 *

* * @author your-name */ @Configuration @MapperScan("com.zwnsyw.zwwwspringbootbasetemplate.mapper") @Slf4j public class MyBatisPlusConfig { /** * 配置 MyBatis Plus 插件 *

* 注意:插件的添加顺序会影响执行顺序 * 建议顺序:多租户 -> 动态表名 -> 分页 -> 乐观锁 -> SQL性能规范 *

* * @return MybatisPlusInterceptor */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // ==================== 分页插件 ==================== PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); // 设置最大单页限制数量,-1 不受限制 paginationInterceptor.setMaxLimit(500L); // 溢出总页数后是否进行处理(true:回到第一页) paginationInterceptor.setOverflow(false); interceptor.addInnerInterceptor(paginationInterceptor); // ==================== 乐观锁插件 ==================== // 配合实体类 @Version 注解使用 // 更新时自动检查版本号,防止并发更新冲突 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } /** * 字段自动填充处理器 *

* 配合实体类 @TableField(fill = FieldFill.INSERT) 等注解使用 * 自动填充创建时间、更新时间、创建人、更新人等字段 *

* * @return MetaObjectHandler */ @Bean public MetaObjectHandler metaObjectHandler() { return new MetaObjectHandler() { /** * 插入时自动填充 */ @Override public void insertFill(MetaObject metaObject) { log.debug("开始插入填充..."); // 创建时间 this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 更新时间 this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 创建人(需要从上下文获取当前登录用户ID) // Long currentUserId = UserContext.getCurrentUserId(); // if (currentUserId != null) { // this.strictInsertFill(metaObject, "createBy", Long.class, currentUserId); // this.strictInsertFill(metaObject, "updateBy", Long.class, currentUserId); // } // 逻辑删除初始值 this.strictInsertFill(metaObject, "isDeleted", Integer.class, 0); } /** * 更新时自动填充 */ @Override public void updateFill(MetaObject metaObject) { log.debug("开始更新填充..."); // 更新时间 this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 更新人 // Long currentUserId = UserContext.getCurrentUserId(); // if (currentUserId != null) { // this.strictUpdateFill(metaObject, "updateBy", Long.class, currentUserId); // } } }; } } ``` #### `RedisConfig.java` ```java package com.example.projectname.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; /** * Redis 配置 *

* 配置 RedisTemplate 的序列化方式,解决: * 1. 默认 JDK 序列化可读性差、占用空间大的问题 * 2. 类型信息丢失导致反序列化失败的问题 *

* * @author your-name */ @Configuration @Slf4j public class RedisConfig { /** * 配置 RedisTemplate *

* Key 使用 String 序列化 * Value 使用 JSON 序列化(携带类型信息) *

* * @param connectionFactory Redis 连接工厂 * @return RedisTemplate */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // Key 序列化:使用 String template.setKeySerializer(RedisSerializer.string()); template.setHashKeySerializer(RedisSerializer.string()); // Value 序列化:使用 JSON GenericJackson2JsonRedisSerializer valueSerializer = createJsonSerializer(); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.setDefaultSerializer(valueSerializer); // 开启事务支持(可选,需要配合 @Transactional 使用) // template.setEnableTransactionSupport(true); template.afterPropertiesSet(); log.info("RedisTemplate 配置完成,使用 JSON 序列化"); return template; } /** * 配置 StringRedisTemplate *

* Key 和 Value 都使用 String 序列化 * 适用于简单的字符串存储场景 *

* * @param connectionFactory Redis 连接工厂 * @return StringRedisTemplate */ @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(connectionFactory); return template; } /** * 创建 JSON 序列化器 *

* 配置类型白名单,只允许序列化/反序列化指定包下的类 * 防止反序列化漏洞攻击 *

* * @return GenericJackson2JsonRedisSerializer */ private GenericJackson2JsonRedisSerializer createJsonSerializer() { ObjectMapper objectMapper = new ObjectMapper(); // 设置所有字段可见(包括私有字段) objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 配置多态类型验证器(安全白名单) BasicPolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder() // ========== 允许的业务包路径(修改为你的包路径) ========== .allowIfSubType("com.example.projectname") // ========== 允许的 Java 集合类型 ========== .allowIfSubType("java.util.ArrayList") .allowIfSubType("java.util.LinkedList") .allowIfSubType("java.util.HashMap") .allowIfSubType("java.util.LinkedHashMap") .allowIfSubType("java.util.TreeMap") .allowIfSubType("java.util.HashSet") .allowIfSubType("java.util.LinkedHashSet") .allowIfSubType("java.util.TreeSet") // ========== 允许的基础类型 ========== .allowIfSubType("java.lang.String") .allowIfSubType("java.lang.Long") .allowIfSubType("java.lang.Integer") .allowIfSubType("java.lang.Double") .allowIfSubType("java.lang.Boolean") .allowIfSubType("java.math.BigDecimal") .allowIfSubType("java.math.BigInteger") // ========== 允许的日期时间类型 ========== .allowIfSubType("java.util.Date") .allowIfSubType("java.sql.Timestamp") .allowIfSubType("java.time.LocalDate") .allowIfSubType("java.time.LocalDateTime") .allowIfSubType("java.time.LocalTime") // ========== 允许的第三方类型 ========== .allowIfSubType("com.baomidou.mybatisplus.extension.plugins.pagination.Page") .build(); // 启用默认类型(存储类型信息,支持多态) objectMapper.activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL); return new GenericJackson2JsonRedisSerializer(objectMapper); } } ``` #### `SessionConfig.java` ```java package com.example.projectname.config; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; /** * Spring Session 配置 *

* 使用 Redis 存储 Session,支持分布式会话管理 *

*

* maxInactiveIntervalInSeconds:Session 过期时间(秒) * - 86400 = 1天 * - 604800 = 7天 * - 2592000 = 30天 *

* * @author your-name */ @Configuration @EnableRedisHttpSession( // Session 过期时间:30天 maxInactiveIntervalInSeconds = 86400 * 30, // Redis 中 Session 的命名空间前缀 redisNamespace = "session" ) public class SessionConfig { /** * 配置 Session 序列化器 *

* 使用 JSON 格式存储 Session 数据,便于查看和调试 *

* * @return RedisSerializer */ @Bean @Qualifier("springSessionDefaultRedisSerializer") public RedisSerializer springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } // ==================== Session 相关说明 ==================== // // 1. Session 存储的数据结构: // - spring:session:sessions:{sessionId} -> Session 数据 // - spring:session:expirations:{时间戳} -> 过期时间索引 // - spring:session:sessions:expires:{sessionId} -> 过期标记 // // 2. 获取 Session: // HttpSession session = request.getSession(); // session.setAttribute("user", userInfo); // Object user = session.getAttribute("user"); // // 3. 使 Session 失效(登出): // session.invalidate(); // // 4. 前端需要: // - 请求时携带 Cookie(credentials: 'include') // - 或使用自定义 Header 传递 Session ID } ``` #### `WebMvcConfig.java` ```java package com.example.projectname.config; import com.example.projectname.interceptor.AuthInterceptor; import com.example.projectname.interceptor.LogInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Arrays; import java.util.List; /** * Spring MVC 配置 *

* 配置拦截器、静态资源映射等 *

* * @author your-name */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { /** * 认证拦截器 */ @Autowired(required = false) private AuthInterceptor authInterceptor; /** * 日志拦截器 */ @Autowired(required = false) private LogInterceptor logInterceptor; /** * 不需要拦截的路径(白名单) */ private static final List EXCLUDE_PATHS = Arrays.asList( // 登录注册相关 "/user/login", "/user/register", "/user/captcha", // 静态资源 "/static/**", "/favicon.ico", // 接口文档 "/doc.html", "/swagger-resources/**", "/webjars/**", "/v2/api-docs", "/v3/api-docs/**", // 健康检查 "/actuator/**", // 错误页面 "/error" ); /** * 配置拦截器 * * @param registry 拦截器注册器 */ @Override public void addInterceptors(InterceptorRegistry registry) { // ========== 日志拦截器(记录请求日志) ========== if (logInterceptor != null) { registry.addInterceptor(logInterceptor) .addPathPatterns("/**") .order(0); // 优先级最高 } // ========== 认证拦截器(权限校验) ========== if (authInterceptor != null) { registry.addInterceptor(authInterceptor) .addPathPatterns("/**") // 拦截所有路径 .excludePathPatterns(EXCLUDE_PATHS) // 排除白名单 .order(1); } // ========== 其他拦截器示例 ========== // registry.addInterceptor(new RateLimitInterceptor()) // .addPathPatterns("/**") // .order(2); } /** * 配置静态资源映射 * * @param registry 资源处理器注册器 */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // Knife4j 接口文档静态资源 registry.addResourceHandler("doc.html") .addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); // 自定义静态资源路径 // registry.addResourceHandler("/uploads/**") // .addResourceLocations("file:/data/uploads/"); } // ==================== 其他可配置项 ==================== // /** // * 配置视图解析器 // */ // @Override // public void configureViewResolvers(ViewResolverRegistry registry) { // registry.jsp("/WEB-INF/views/", ".jsp"); // } // /** // * 配置消息转换器 // */ // @Override // public void configureMessageConverters(List> converters) { // // 添加自定义消息转换器 // } } ``` #### `Knife4jConfig.java` ```java package com.zwnsyw.zwwwspringbootbasetemplate.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; /** * Knife4j / OpenAPI 3.0 接口文档配置 *

* 访问地址:http://localhost:8080/api/doc.html *

*

* 基于 Knife4j 4.4.0 (OpenAPI 3.0) * 特点: * - 无需 Springfox 依赖 * - 自动扫描所有 @RestController 接口 * - 支持 @Tag、@Operation 等 OpenAPI 3.0 注解 * - 生产环境通过 knife4j.enable=false 禁用 *

* * @author zwww */ @Configuration @Profile({"dev", "test"}) // 仅在开发和测试环境启用 public class Knife4jConfig { /** * 配置 OpenAPI 3.0 文档信息 *

* 注意:Knife4j OpenAPI 3.0 会自动扫描所有 Controller * 无需手动配置 Docket 或路径选择器 *

* * @return OpenAPI 文档对象 */ @Bean public OpenAPI customOpenAPI() { return new OpenAPI() // ==================== 基本信息 ==================== .info(new Info() .title("项目接口文档") .version("1.0.0") .description("RESTful API 接口说明文档") // 联系人信息 .contact(new Contact() .name("zwww") .url("https://zwnsyw.top") .email("2446796988@qq.com")) // 许可证信息 .license(new License() .name("Apache 2.0") .url("http://www.apache.org/licenses/LICENSE-2.0"))) // ==================== JWT 安全认证配置 ==================== .addSecurityItem(new SecurityRequirement().addList("Bearer Token")) .components(new io.swagger.v3.oas.models.Components() .addSecuritySchemes("Bearer Token", new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT") .description("JWT Token"))); } // ==================== 配置说明 ==================== /** * 在 Controller 中使用注解: * * @RestController * @RequestMapping("/api/users") * @Tag(name = "用户管理", description = "用户相关接口") * public class UserController { * * @GetMapping("/{id}") * @Operation(summary = "获取用户", description = "根据ID获取用户详情") * @Parameter(name = "id", description = "用户ID", required = true) * @ApiResponse(responseCode = "200", description = "成功") * public UserDTO getUser(@PathVariable Long id) { * return new UserDTO(); * } * * @PostMapping * @Operation(summary = "创建用户") * public UserDTO createUser(@RequestBody UserDTO user) { * return user; * } * } * * @Schema 用于 DTO 字段: * * public class UserDTO { * @Schema(description = "用户ID", example = "1") * private Long id; * * @Schema(description = "用户名", example = "张三") * private String name; * * @Schema(description = "邮箱", example = "test@example.com") * private String email; * } */ } ``` #### `ThreadPoolConfig.java` ```java package com.example.projectname.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; /** * 线程池配置 *

* 配合 @Async 注解使用,实现异步方法调用 *

*

* 使用方式: * 1. 在启动类或配置类上添加 @EnableAsync * 2. 在需要异步执行的方法上添加 @Async("asyncExecutor") *

* * @author your-name */ @Configuration @EnableAsync public class ThreadPoolConfig { /** * 核心线程数 *

* 建议:CPU 密集型任务设为 CPU 核心数 + 1 * IO 密集型任务设为 CPU 核心数 * 2 *

*/ private static final int CORE_POOL_SIZE = 4; /** * 最大线程数 */ private static final int MAX_POOL_SIZE = 10; /** * 队列容量 */ private static final int QUEUE_CAPACITY = 100; /** * 线程空闲时间(秒) */ private static final int KEEP_ALIVE_SECONDS = 60; /** * 线程名称前缀 */ private static final String THREAD_NAME_PREFIX = "async-task-"; /** * 配置异步任务线程池 * * @return Executor */ @Bean("asyncExecutor") public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 核心线程数 executor.setCorePoolSize(CORE_POOL_SIZE); // 最大线程数 executor.setMaxPoolSize(MAX_POOL_SIZE); // 队列容量 executor.setQueueCapacity(QUEUE_CAPACITY); // 线程空闲时间 executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS); // 线程名称前缀 executor.setThreadNamePrefix(THREAD_NAME_PREFIX); // 拒绝策略 // AbortPolicy:直接抛出异常(默认) // CallerRunsPolicy:由调用线程执行 // DiscardPolicy:直接丢弃任务 // DiscardOldestPolicy:丢弃队列中最老的任务 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待所有任务完成后再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 等待时间 executor.setAwaitTerminationSeconds(60); // 初始化 executor.initialize(); return executor; } /** * 定时任务线程池(如果使用 @Scheduled) * * @return Executor */ @Bean("scheduledExecutor") public Executor scheduledExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(4); executor.setQueueCapacity(50); executor.setThreadNamePrefix("scheduled-task-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } ``` #### `AppProperties.java` ```java package com.example.projectname.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.validation.annotation.Validated; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Positive; /** * 自定义业务配置 *

* 集中管理业务相关的配置项,从 application.yml 读取 *

*

* 使用方式: * 1. 在 application.yml 中配置: * app: * name: 项目名称 * file: * max-size: 10485760 * 2. 注入使用: * @Autowired * private AppProperties appProperties; *

* * @author your-name */ @Data @Configuration @ConfigurationProperties(prefix = "app") @Validated public class AppProperties { /** * 应用名称 */ private String name = "project-name"; /** * 应用版本 */ private String version = "1.0.0"; /** * 是否开启调试模式 */ private boolean debug = false; /** * 文件上传配置 */ private FileConfig file = new FileConfig(); /** * JWT 配置 */ private JwtConfig jwt = new JwtConfig(); /** * 用户相关配置 */ private UserConfig user = new UserConfig(); // ==================== 内部配置类 ==================== /** * 文件上传配置 */ @Data public static class FileConfig { /** * 文件最大大小(字节) * 默认 10MB */ @Positive(message = "文件大小必须为正数") private long maxSize = 10 * 1024 * 1024; /** * 允许的文件格式 */ private String allowedFormats = "jpg,jpeg,png,gif,webp,pdf"; /** * 上传路径 */ private String uploadPath = "/uploads"; } /** * JWT 配置 */ @Data public static class JwtConfig { /** * 密钥 */ @NotBlank(message = "JWT 密钥不能为空") private String secret = "your-default-secret-key-please-change-in-production"; /** * 过期时间(秒) * 默认 7 天 */ @Positive(message = "过期时间必须为正数") private long expiration = 7 * 24 * 60 * 60; /** * Token 前缀 */ private String tokenPrefix = "Bearer "; /** * Header 名称 */ private String headerName = "Authorization"; } /** * 用户相关配置 */ @Data public static class UserConfig { /** * 密码最大重试次数 */ private int maxPasswordRetry = 5; /** * 最大登录设备数 */ private int maxLoginDevice = 3; /** * 账户锁定时间(分钟) */ private int lockMinutes = 30; } } ``` ## **配置类总览** | **配置类** | **作用** | **关键配置项** | | --- | --- | --- | | **CacheConfig** | Spring Cache 缓存配置 | 缓存过期时间、序列化方式 | | **CorsConfig** | 跨域配置 | 允许的域名、方法、请求头 | | **JsonConfig** | JSON 序列化配置 | Long 转 String、日期格式化 | | **MyBatisPlusConfig** | ORM 配置 | 分页、乐观锁、自动填充 | | **RedisConfig** | Redis 配置 | 序列化方式、类型白名单 | | **SessionConfig** | 分布式会话配置 | 过期时间、序列化方式 | | **WebMvcConfig** | MVC 配置 | 拦截器、静态资源映射 | | **Knife4jConfig** | 接口文档配置 | 文档信息、扫描范围 | | **ThreadPoolConfig** | 线程池配置 | 核心线程数、队列容量 | | **AppProperties** | 业务配置 | 文件上传、JWT、用户配置 | ### **使用建议** 1. **按需引入**:根据项目需要选择配置类 2. **修改包路径**:将 `com.example.projectname` 替换为你的实际包路径 3. **敏感信息外置**:密钥等敏感信息通过环境变量配置 4. **生产环境**:关闭接口文档、调整日志级别 --- ## **7. 最佳实践总结** ### **7.1 命名规范** | **类型** | **命名格式** | **示例** | | --- | --- | --- | | Entity | `XxxEntity` 或 `Xxx` | `User`、`Team` | | DTO | `XxxDTO` 或 `XxxRequest` | `UserRegisterDTO` | | VO | `XxxVO` | `UserVO` | | Enum | `XxxEnum` | `GenderEnum` | | Service | `XxxService` | `UserService` | | ServiceImpl | `XxxServiceImpl` | `UserServiceImpl` | | Controller | `XxxController` | `UserController` | | Mapper | `XxxMapper` | `UserMapper` | ### **8.2 分层职责** ```text ┌─────────────┐ │ Controller │ → 参数接收、非空校验、触发DTO校验、调用Service ├─────────────┤ │ Service │ → 业务逻辑、业务校验、事务管理、调用Mapper ├─────────────┤ │ Mapper │ → 数据库操作(CRUD) ├─────────────┤ │ Entity │ → 数据库表映射 ├─────────────┤ │ DTO │ → 请求数据封装、字段校验 ├─────────────┤ │ VO │ → 响应数据封装、脱敏处理 └─────────────┘ ``` ### **8.3 校验注解速查** | **注解** | **作用** | **示例** | | --- | --- | --- | | `@NotNull` | 不能为 null | `@NotNull Long id` | | `@NotBlank` | 字符串非空且非空白 | `@NotBlank String name` | | `@NotEmpty` | 集合/数组非空 | `@NotEmpty List tags` | | `@Size` | 长度/大小范围 | `@Size(min=1, max=10)` | | `@Min` / `@Max` | 数值范围 | `@Min(0) @Max(100)` | | `@Email` | 邮箱格式 | `@Email String email` | | `@Pattern` | 正则匹配 | `@Pattern(regexp="...")` | | `@AssertTrue` | 自定义校验方法 | `@AssertTrue isValid()` | | `@Valid` | 触发嵌套校验 | `@Valid AddressDTO address` | | `@Validated` | 分组校验 | `@Validated(UpdateGroup.class)` | ### **8.4 枚举设计要点** 1. **@EnumValue**:标记存储到数据库的字段 2. **@JsonValue**:标记序列化到 JSON 的字段 3. **@JsonCreator**:标记反序列化的工厂方法 4. **fromCode()**:提供根据 code 获取枚举的静态方法 5. **isValid()**:提供验证 code 是否有效的方法 ### **8.5 MybatisX 代码生成使用建议** 1. 生成到临时目录,手动迁移到项目中 2. Entity 生成后需添加枚举字段映射 3. Mapper 和 Service 可直接使用生成结果 4. 按需修改生成的代码,添加业务逻辑 --- **项目分区导航**:⬅️ [[01-Spring Boot MVC 项目最佳实践规范|01-Spring Boot MVC 项目最佳实践规范]] | 02-SpringBoot MVC 分层最佳实践 | ➡️ [[03-优雅配置管理方案|03-优雅配置管理方案]]